/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.wsinfra.wse.impl;

import java.util.Calendar;

import cgl.narada.wsinfra.WsMessageFlow;
import cgl.narada.wsinfra.WsProcessor;
import cgl.narada.wsinfra.exception.DeploymentException;
import cgl.narada.wsinfra.exception.IncorrectExchangeException;
import cgl.narada.wsinfra.exception.MessageFlowException;
import cgl.narada.wsinfra.exception.ParsingException;
import cgl.narada.wsinfra.exception.ProcessingException;
import cgl.narada.wsinfra.exception.UnknownExchangeException;
import cgl.narada.wsinfra.exception.WsFaultException;
import cgl.narada.wsinfra.schemas.addressing.ActionDocument;
import cgl.narada.wsinfra.schemas.addressing.EndpointReferenceType;
import cgl.narada.wsinfra.schemas.addressing.RelatesToDocument;
import cgl.narada.wsinfra.schemas.eventing.FilterType;
import cgl.narada.wsinfra.schemas.eventing.GetStatusDocument;
import cgl.narada.wsinfra.schemas.eventing.GetStatusResponseDocument;
import cgl.narada.wsinfra.schemas.eventing.IdentifierDocument;
import cgl.narada.wsinfra.schemas.eventing.RenewDocument;
import cgl.narada.wsinfra.schemas.eventing.RenewResponseDocument;
import cgl.narada.wsinfra.schemas.eventing.SubscribeDocument;
import cgl.narada.wsinfra.schemas.eventing.SubscribeResponseDocument;
import cgl.narada.wsinfra.schemas.eventing.UnsubscribeDocument;
import cgl.narada.wsinfra.schemas.soap.EnvelopeDocument;
import cgl.narada.wsinfra.util.QNameLocator;
import cgl.narada.wsinfra.wsa.AddressingHeaders;
import cgl.narada.wsinfra.wsa.ParseWsaHeaders;
import cgl.narada.wsinfra.wsa.WsaEprRules;
import cgl.narada.wsinfra.wsa.WsaFaultRules;
import cgl.narada.wsinfra.wsa.WsaProcessingFactory;
import cgl.narada.wsinfra.wse.WseActions;
import cgl.narada.wsinfra.wse.WseElementCreation;
import cgl.narada.wsinfra.wse.WseFaults;
import cgl.narada.wsinfra.wse.WseNodeUtils;
import cgl.narada.wsinfra.wse.WseProcessingFactory;
import cgl.narada.wsinfra.wse.WseQNames;
import cgl.narada.wsinfra.wse.WseResponseCreator;
import cgl.narada.wsinfra.wse.WseSubscriptionManager;
import cgl.narada.wsinfra.wse.matching.SubscriptionEntry;
import cgl.narada.wsinfra.wse.matching.SubscriptionEntryFactory;
import cgl.narada.wsinfra.wse.matching.SubscriptionExpiryListener;
import cgl.narada.wsinfra.wse.matching.SubscriptionManagement;
import cgl.narada.wsinfra.wse.matching.SubscriptionPolicies;

/** This class encapsulates the behavior of a WS-Eventing subscription manager
    node.

    @author Shrideep Pallickara
    $Date: 2005/07/29 22:41:30 $
    $Revision: 1.12 $
*/


public class WseSubscriptionManagerProcessor extends WsProcessor 
  implements WseSubscriptionManager, SubscriptionExpiryListener {

  private static final WseSubscriptionManagerProcessor instance =
  new WseSubscriptionManagerProcessor();
  
  private ParseWsaHeaders parseWsaHeaders;
  private WsaFaultRules wsaFaultRules;
  private WsaEprRules wsaEprRules;

  private WseActions wseActions;
  private WseQNames wseQNames;
  private WseFaults wseFaults;
  private WseResponseCreator wseResponseCreator;
  private WseElementCreation wseElementCreation;

  private SubscriptionManagement subscriptionManagement;
  private SubscriptionEntryFactory subscriptionEntryFactory;
  private SubscriptionPolicies subscriptionPolicies;

  private WseNodeUtils wseNodeUtils;

  private QNameLocator qNameLocator;
  private EndpointReferenceType subscriptionManagerEpr;

  private WsMessageFlow wsMessageFlow;
  private String moduleName = "WseSubscriptionManagerProcessor: ";

  private WseSubscriptionManagerProcessor() {
    parseWsaHeaders = WsaProcessingFactory.getParseWsaHeaders();
    wsaFaultRules =  WsaProcessingFactory.getWsaFaultRules();
    wsaEprRules =  WsaProcessingFactory.getWsaEprRules();

    wseActions = WseActions.getInstance();
    wseQNames = WseQNames.getInstance();
    wseFaults = WseFaults.getInstance();
    wseNodeUtils = WseProcessingFactory.getWseNodeUtils();
    wseResponseCreator = WseProcessingFactory.getWseResponseCreator();
    wseElementCreation = WseProcessingFactory.getWseElementCreation();
    
    qNameLocator = QNameLocator.getInstance();

    subscriptionManagement = new SubscriptionManagement();
    subscriptionEntryFactory = SubscriptionEntryFactory.getInstance();
    subscriptionPolicies =  SubscriptionPolicies.getInstance();
  }


  public static WseSubscriptionManagerProcessor getInstance() {
    return instance;
  }


  /** Sets the message flow which the processor should use */
  public void setMessageFlow(WsMessageFlow wsMessageFlow) 
    throws DeploymentException {
    this.wsMessageFlow = wsMessageFlow;
  }
  
  /** Gets the message flow which the processor should use. */
  public WsMessageFlow getMessageFlow() {
    return wsMessageFlow;
  }


  public void
  setSubscriptionManagerEPR(EndpointReferenceType subscriptionManagerEpr) {
    this.subscriptionManagerEpr = subscriptionManagerEpr;
  }

  /** Process the exchange. The argument also indicates the direction in
      which the exchange has actually traversed. */
  public boolean processExchange(EnvelopeDocument envelopeDocument,
			      int direction)
    throws UnknownExchangeException, IncorrectExchangeException,
    ProcessingException, MessageFlowException {

    AddressingHeaders addressingHeaders;

    try {
      addressingHeaders =
	parseWsaHeaders.getAddressingHeaders(envelopeDocument);
    } catch (ParsingException parseEx) {
      throw new ProcessingException(moduleName + "Unable to process. " +
				    "Problems related to parsing");
    }

    ActionDocument actionDocument = addressingHeaders.getAction();
    if (actionDocument == null) {
      String reason = moduleName +
	"Action missing. Processor only deals with WS-Eventing exchanges.";
      throw new UnknownExchangeException(reason);
    }

    String action = null;
    if (actionDocument != null) {
      action = actionDocument.getAction().getStringValue();
    }
    checkExchangeType(action, direction);

    /** From here on we are ensured that only valid exchanges are being
	processed. */

    try {
      if (action.equals( wseActions.getSubscribeResponse()) ) {
	processSubscriptionAddition(envelopeDocument, addressingHeaders);
      }

      if (action.equals( wseActions.getSubscriptionEnd()) ) {
	processSubscriptionEnd(envelopeDocument, addressingHeaders);
      }

      /** Process renewals */
      if (action.equals( wseActions.getRenew()) ) {
      	processSubscriptionRenewal(envelopeDocument, addressingHeaders);
      }

      if (action.equals(wseActions.getGetStatus()) ) {
	processSubscriptionStatus(envelopeDocument, addressingHeaders);
      }

      if (action.equals(wseActions.getUnsubscribe())) {
	processSubscriptionUnsubscribe(envelopeDocument, addressingHeaders);
      }

      if (action.equals( wseActions.getSubscribeResponse()) ) {
	processSubscriptionAddition(envelopeDocument, addressingHeaders);
      }

    } catch(WsFaultException wsFaultException) {
      /** If message flow is initialized, create appropriate fault to send*/
      EndpointReferenceType faultTo =
	wsaFaultRules.sendFaultTo(addressingHeaders);
      wsFaultException.setFaultTo(faultTo);
      manageFaultException(wsFaultException, direction);
      System.out.println(moduleName + wsFaultException);
    }
    
    return false;
  }

 


  public void processSubscriptionRenewal(EnvelopeDocument envelopeDocument,
					 AddressingHeaders addressingHeaders)
    throws ProcessingException, WsFaultException, MessageFlowException  {

    RenewDocument renewDocument =
      wseNodeUtils.getRenewDocument(envelopeDocument);
    RenewDocument.Renew renew = renewDocument.getRenew();

    IdentifierDocument idDocument =
      wseNodeUtils.getIdentifierDocument(envelopeDocument);
    String subscriptionId = idDocument.getIdentifier();
    System.out.println(moduleName + "Subscription identifier with request =>"
		       + subscriptionId);
    checkSubscriptionIdentifierForProblems(subscriptionId);

    SubscriptionEntry subscriptionEntry = 
      subscriptionManagement.getEntry(subscriptionId);

    Object expiresAtObject = renew.getExpires();
    Calendar expiresAt = null;
    if (expiresAtObject != null) {
      expiresAt = (Calendar) expiresAtObject;
    }
    
    Calendar renewal = subscriptionPolicies.getRenewal(expiresAt);
    subscriptionEntryFactory.updateSubscriptionEntry(subscriptionEntry, 
						     renewal);
    RenewResponseDocument renewResponseDocument =
      wseElementCreation.newRenewResponseDocument(subscriptionEntry);
    
    String requestType = "[wse:Renew]";
    EndpointReferenceType alternateEpr = subscriptionEntry.getSink();
    EndpointReferenceType responseTo = 
      wseResponseCreator.getSendResponseTo(addressingHeaders, alternateEpr, 
					   requestType);
    RelatesToDocument relatesTo = 
      wseResponseCreator.getResponseRelatesTo(addressingHeaders);

    EnvelopeDocument renewResponseEnvelope = 
      wseResponseCreator.createRenewResponse(responseTo, 
					     subscriptionManagerEpr, 
					     relatesTo,renewResponseDocument);
    enrouteToNetwork(renewResponseEnvelope);

    
    EndpointReferenceType sourceEpr = subscriptionEntry.getSource();
    EnvelopeDocument renewResponseEnvelopeToSource =
      wseResponseCreator.createRenewResponseToSource(sourceEpr, 
						     subscriptionManagerEpr, 
						     subscriptionId, 
						     renewResponseDocument);
    enrouteToNetwork(renewResponseEnvelopeToSource);
  }
  
  
  /** Process a request to get the status of a subscription. This method
      will throw exceptions (or generate faults) if the identifier contained 
      in the subscription GetStatus is an unknown one.*/
  public void processSubscriptionStatus(EnvelopeDocument envelopeDocument,
					AddressingHeaders addressingHeaders)
    throws ProcessingException, WsFaultException, MessageFlowException {
    
    GetStatusDocument getStatusDocument =
      wseNodeUtils.getGetStatusDocument(envelopeDocument);
    IdentifierDocument idDocument =
      wseNodeUtils.getIdentifierDocument(envelopeDocument);
    
    String subscriptionId = idDocument.getIdentifier();
    System.out.println(moduleName + "Requesting status for subscriptionID =>"
		       + subscriptionId);
    
    checkSubscriptionIdentifierForProblems(subscriptionId);
    
    SubscriptionEntry subscriptionEntry = 
      subscriptionManagement.getEntry(subscriptionId);
    
    GetStatusResponseDocument getStatusResponseDocument = 
      wseElementCreation.newGetStatusResponseDocument(subscriptionEntry);
    
    String requestType = "[wse:GetStatus]";
    EndpointReferenceType responseTo = 
      wseResponseCreator.getSendResponseTo(addressingHeaders, 
					   subscriptionEntry.getSink(),
					   requestType);
    RelatesToDocument relatesTo = 
      wseResponseCreator.getResponseRelatesTo(addressingHeaders);

    EnvelopeDocument getStatusResponse = 
      wseResponseCreator.createGetStatusResponse(responseTo, 
						 subscriptionManagerEpr,
						 relatesTo,
						 getStatusResponseDocument);
    enrouteToNetwork(getStatusResponse);
  }



  public void
  processSubscriptionUnsubscribe(EnvelopeDocument envelopeDocument,
				 AddressingHeaders addressingHeaders)
    throws ProcessingException, WsFaultException, MessageFlowException {
    UnsubscribeDocument unsubscribeDocument =
      wseNodeUtils.getUnsubscribeDocument(envelopeDocument);
    IdentifierDocument idDocument =
      wseNodeUtils.getIdentifierDocument(envelopeDocument);

    String subscriptionId = idDocument.getIdentifier();
    System.out.println(moduleName + "Trying to unsubscribe subscriptionID =>"
		       + subscriptionId);

    checkSubscriptionIdentifierForProblems(subscriptionId);    
    SubscriptionEntry subscriptionEntry = 
      subscriptionManagement.deleteEntry(subscriptionId);
    
    String requestType = "[wse:Unsubscribe]";
    EndpointReferenceType responseTo = 
      wseResponseCreator.getSendResponseTo(addressingHeaders, 
					   subscriptionEntry.getSink(),
					   requestType);
    RelatesToDocument relatesTo = 
      wseResponseCreator.getResponseRelatesTo(addressingHeaders);
    
    EnvelopeDocument unsubscribeResponse = 
      wseResponseCreator.createUnsubscribeResponse(responseTo, 
						   subscriptionManagerEpr,
						   relatesTo);
    enrouteToNetwork(unsubscribeResponse);

    EndpointReferenceType sourceEpr = subscriptionEntry.getSource();
    EnvelopeDocument unsubscribeResponseToSource = 
      wseResponseCreator.
      createUnsubscribeResponseToSource(sourceEpr,
					subscriptionManagerEpr,subscriptionId);
    enrouteToNetwork(unsubscribeResponseToSource);

  }





  /** This method is used to notify a subscription manager of the a
      successuful subscription request-response combination.
      The request (issued by the sink) allows a subscription manager to know
      about the subscription, while the response (issued by the source) allows
      it know the wse:Identifer for this subscription and also the expiry
      associated with the subscription. Also note that the wse:Identifier is
      what is used by the sinks in all their interactions with the subscription
      manager */
  public void processSubscriptionAddition(EnvelopeDocument envelopeDocument,
					  AddressingHeaders addressingHeaders)
    throws WsFaultException, MessageFlowException {
    String faultReason = "";
    SubscribeResponseDocument subscribeResponseDocument =
      wseNodeUtils.getSubscribeResponseDocument(envelopeDocument);
    SubscribeDocument subscribeDocument =
      wseNodeUtils.getSubscribeDocument(envelopeDocument);

    /** The identifier for this subscription would be contained in the
	subscribeResponse which is contained within the envelope body. */
    IdentifierDocument idDocument =
      wseNodeUtils.getIdentifierDocumentFromBody(envelopeDocument);

    String subscriptionId = idDocument.getIdentifier();
    if (subscriptionId == null) {
      faultReason = "No subscription identifier has been specified " +
	"OR it is NULL. Please check your request again.";
      throwInvalidMessageFault(faultReason);
    }

    
    SubscribeDocument.Subscribe subscribe = subscribeDocument.getSubscribe();
    FilterType filter = subscribe.getFilter();
    
    SubscribeResponseDocument.SubscribeResponse subscribeResponse =
      subscribeResponseDocument.getSubscribeResponse();
    Object expiresAtObject = subscribeResponse.getExpires(); 
    Calendar expiresAt = null;
    if (expiresAtObject != null) {
      expiresAt = (Calendar) expiresAtObject;
    }

    EndpointReferenceType notifyTo=wseNodeUtils.getNotifyTo(subscribeDocument);
    
    /** Processing related to EndTo. If this is PRESENT SubscriptionEnd
	notifications are expected and must be sent to the location specified
	in the endTo. */
    EndpointReferenceType endTo = null;
    boolean hasEndTo = subscribe.isSetEndTo();
    if (hasEndTo) {
      endTo = subscribe.getEndTo();
    }
    
    EndpointReferenceType sourceEpr = wsaEprRules.sendTo(addressingHeaders);
    SubscriptionEntry subscribeEntry =
      subscriptionEntryFactory.createEntry(subscriptionId, filter, expiresAt,
					   sourceEpr, notifyTo, endTo,
					   subscriptionManagerEpr);
    
    subscriptionManagement.addEntry(subscribeEntry);
  }



  /** This method is used to notify a subscription manager of a subscription
      end. The wse:Identifer for this subscription can be retrieve from this
      message. */
  private void processSubscriptionEnd(EnvelopeDocument envelopeDocument,
				      AddressingHeaders addressingHeaders)
    throws WsFaultException, MessageFlowException {
    String faultReason = "";
    /** The identifier for this subscription would be contained in the
	subscribeResponse which is contained within the envelope body. */
    IdentifierDocument idDocument =
      wseNodeUtils.getIdentifierDocumentFromBody(envelopeDocument);

    String subscriptionId = idDocument.getIdentifier();
    checkSubscriptionIdentifierForProblems(subscriptionId); 

    subscriptionManagement.deleteEntry(subscriptionId);

  }
  


  /** Process the a GetStatus request issued by a sink. A fault may be thrown
      if there are problems processing this request.
      wse:Identifer is needed.
  */
  public GetStatusResponseDocument
  processGetStatusOfSubscription(GetStatusDocument getStatusDocument )
    throws WsFaultException {

    return null;
  }



  /** Process a renew request issued by a sink. A fault may be thrown
      if there are problems processing this request.
      wse:Identifier is needed */
  public RenewResponseDocument
  processRenewSubscription(RenewDocument renewDocument)
    throws WsFaultException {

    return null;
  }



  /** Process an unsubscribe request issued by a sink. A fault may be thrown
      if there are problems processing this request.
      wse:Identifier is needed */
  public EnvelopeDocument
  processUnsubscribe(UnsubscribeDocument unsubscribeDocument)
    throws WsFaultException {

    return null;
  }


  /** Method that is called when one of the managed subscriptions expires */
  public void onSubscriptionExpiry(SubscriptionEntry expiredEntry) {


  }

  /** Check the specified subscription identifier for problems. Specifically
      throw an wse:InvalidFault if there are problems. */
  private void  
  checkSubscriptionIdentifierForProblems(String subscriptionIdentifier)
    throws WsFaultException {
    String faultReason = "";

    if (subscriptionIdentifier == null) {
      faultReason = "No subscription identifier has been specified or it is" +
	" NULL. Please check your request again.";
      throwInvalidMessageFault(faultReason);
    }

    if (!subscriptionManagement.hasEntry(subscriptionIdentifier)) {
      faultReason = "The specified subscription identifier [" +  
	subscriptionIdentifier + "] is not known to the SubscriptionManager."+
	" Please check your request again.";
      throwInvalidMessageFault(faultReason);
    }
  }

  /** Throw a WsFaultException, initialized with the invalid message QName
      and the specified reason */
  private void throwInvalidMessageFault(String reason)
    throws WsFaultException {

    throw
      new WsFaultException(wseFaults.getInvalidMessage(), reason);
  }


  private void checkExchangeType(String action, int direction)
    throws UnknownExchangeException, IncorrectExchangeException {

    if (direction == WsMessageFlow.FROM_NETWORK) {
      checkExchangeFromNetwork(action);
    }

    if (direction == WsMessageFlow.FROM_APPLICATION) {
      checkExchangeFromApplication(action);
    }

  }



  private void checkExchangeFromNetwork(String action)
    throws UnknownExchangeException, IncorrectExchangeException {
    String from = "Network. ";
    if (action == null) {
      throwUnknownExchangeException(action, from);
    }

    if (action.equals(wseActions.getSubscribe()) ||
	action.equals(wseActions.getGetStatusResponse()) ||
	action.equals(wseActions.getRenewResponse())  ) {
      throwIncorrectExchangeException(action, from);
    }

  }


  private void checkExchangeFromApplication(String action)
    throws UnknownExchangeException, IncorrectExchangeException {
    String from = "Application. ";
    if (action == null) {
      return;
    }

    if (action.equals(wseActions.getSubscribe()) ||
	action.equals(wseActions.getUnsubscribe()) ||
	action.equals(wseActions.getSubscribeResponse()) ||
	action.equals(wseActions.getGetStatus()) ||
	action.equals(wseActions.getGetStatusResponse()) ||
	action.equals(wseActions.getRenew()) ||
	action.equals(wseActions.getRenewResponse()) ) {
      throwIncorrectExchangeException(action, from);
    }

  }

  
  private void throwIncorrectExchangeException(String action, String from)
    throws IncorrectExchangeException {
    String reason =  moduleName +
      "Should NOT have received exchange corresponding to action [" +
      action + "] from the " + from;
    throw new IncorrectExchangeException(reason);
  }


  private void throwUnknownExchangeException(String action, String from)
    throws UnknownExchangeException {
    String reason =  moduleName +
      "No idea about processing exchange corresponding to action [" +
      action + "] from the " + from + ".";
    throw new UnknownExchangeException(reason);
  }

  
  
  /** Try and the initialize the EPR to send a response message to. This
      is typically used to initialize the [wsa:To] element. The addressing
      headers that are passed here are the headers that were constructed based
      on the original request that came in previously. So this method will 
      look for the [wsa:ReplyTo] or [wsa:From] elements to construct the 
      responses. */
  private EndpointReferenceType 
  initializeToFromRequest(AddressingHeaders addressingHeaders, 
			  EndpointReferenceType alternateEPR, 
			  String requestType) 
    throws ProcessingException {
    
    EndpointReferenceType to = wsaEprRules.sendTo(addressingHeaders);
    
    if (to != null) {
      return to;
    }
    
    
    String errorReport = null;
    if (to == null) {
      errorReport = moduleName + "The " + requestType + " came in without the"
	+ " appropriate [wsa:ReplyTo] or [wsa:From] elements. "; 
      System.out.println(errorReport + "Will try and use alternate EPR");
    }
    
    
    if (alternateEPR == null) {
      errorReport += "Furthermore, the available alternateEPR is also NULL!\n";
      throw new ProcessingException(errorReport);
    }
    
    return alternateEPR;
  } 
  

}
